﻿// <copyright file="DataAbstractor.cs" company="DataAbstractor">
//   (c) Luke Geraghty 2013
// </copyright>
// <summary>
//   Copy data from two unrelated classes.
// </summary>

namespace DataAbstractor
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;

    /// <summary>
    /// Copy data from two unrelated classes.
    /// </summary>
    public class DataAbstractor
    {
        /// <summary>
        /// Maps of maps relating properties of two different classes.
        /// </summary>
        private static readonly Dictionary<KeyValuePair<Type, Type>, MapEntry> Maps = new Dictionary<KeyValuePair<Type, Type>, MapEntry>();

        /// <summary>
        /// The current state of data abstractor.
        /// </summary>
        private static State state = State.Unknown;
        
        /// <summary>
        /// The current state of data abstractor.
        /// </summary>
        private enum State
        {
            /// <summary>
            /// Initial state.
            /// </summary>
            Unknown,

            /// <summary>
            /// Initialise() method called.
            /// </summary>
            Initialising,

            /// <summary>
            /// Initialise() method completed.
            /// </summary>
            Initialised
        }

        /// <summary>
        /// Gets or sets a value indicating whether all properties must be mapped.
        /// </summary>
        /// <value>
        /// Whether all properties must be mapped.
        /// </value>
        public static bool AllPropertiesMustBeMapped { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether all properties must be pre-mapped during initialisation.
        /// </summary>
        /// <value>
        /// Whether all classes must be pre-mapped.
        /// </value>
        public static bool EnforcePreMapping { get; set; }
        
        /// <summary>
        /// Initialise data abstractor.
        /// </summary>
        public static void Initialise()
        {
            if (state != State.Unknown)
            {
                return;
            }

            DataAbstractor.state = State.Initialising;

            var mappingClasses = AppDomain.CurrentDomain.GetAssemblies().SelectMany(i => i.GetTypes()).Where(i => !i.IsInterface && typeof(IDataAbstractorMapping).IsAssignableFrom(i)).Select(i => Activator.CreateInstance(i));
            foreach (IDataAbstractorMapping map in mappingClasses)
            {
                map.InitialiseMapping();
            }

            if (AllPropertiesMustBeMapped)
            {
                foreach (var map in Maps.Values)
                {
                    if (!map.IsFullyMapped)
                    {
                        var notMapped = map.FieldsNotMapped.Aggregate(string.Empty, (s, s1) => string.Concat(s, string.IsNullOrEmpty(s) ? string.Empty : ", ", s1));
                        throw new MapException(string.Format(CultureInfo.CurrentCulture, "The mapping for type {0} to {1} is incomplete. Please map the fields: {2}", map.Source, map.Destination, notMapped));
                    }
                }
            }

            DataAbstractor.state = State.Initialised;
        }
        
        /// <summary>
        /// Create a mapping between TDestination and TSource.
        /// </summary>
        /// <typeparam name="TDestination">The destination type.</typeparam>
        /// <typeparam name="TSource">The source type.</typeparam>
        /// <returns>The mapping between TDestination and TSource.</returns>
        public static IMapFrom<TDestination, TSource> Map<TDestination, TSource>()
        {
            if (EnforcePreMapping && state != State.Initialising)
            {
                throw new MapException("Unable to create mapping because EnforcePreMapping is enabled and the mapping must be defined during DataAbstractor.Initialise()");
            }

            var map = GetMap(typeof(TDestination), typeof(TSource));
            return new MapFrom<TDestination, TSource>(map);
        }
        
        /// <summary>
        /// Remove mapping between destination and source type.
        /// </summary>
        /// <typeparam name="TDestination">The destination type.</typeparam>
        /// <typeparam name="TSource">The source type.</typeparam>
        public static void RemoveMapping<TDestination, TSource>()
        {
            RemoveMapping(typeof(TDestination), typeof(TSource));
        }
        
        /// <summary>
        /// Remove mapping between destination and source type.
        /// </summary>
        /// <param name="destination">The destination type.</param>
        /// <param name="source">The source type.</param>
        public static void RemoveMapping(Type destination, Type source)
        {
            destination = StripType(destination);
            source = StripType(source);
            var entry = new KeyValuePair<Type, Type>(destination, source);
            Maps.Remove(entry);
        }

        /// <summary>
        /// Create a new destination from type destinationType and copy the data from source in to that class.
        /// </summary>
        /// <param name="destinationType">The type of class to create.</param>
        /// <param name="source">The source data.</param>
        /// <param name="deepCopy">If true all sub-classes are created.</param>
        /// <returns>The destination object populated with the data from source.</returns>
        public static object To(Type destinationType, object source, bool deepCopy)
        {
            if (destinationType == null)
            {
                throw new ArgumentNullException("destinationType");
            }

            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            if (destinationType.GetInterfaces().Contains(typeof(IEnumerable)))
            {
                var iterativeSource = source as IEnumerable;
                if (iterativeSource == null)
                {
                    return null;
                }

                Type realDestinationType = null;
                if (!destinationType.IsGenericType)
                {
                    realDestinationType = destinationType.GetElementType();
                }
                else if (destinationType.GetGenericTypeDefinition() == typeof(List<>) || destinationType.GetGenericTypeDefinition() == typeof(LinkedList<>))
                {
                    realDestinationType = destinationType.GetGenericArguments()[0];
                }
                
                if (realDestinationType == null)
                {
                    return null;
                }
                
                var newList = typeof(List<>).MakeGenericType(realDestinationType);
                var list = (IList)Activator.CreateInstance(newList);
                foreach (var entry in iterativeSource)
                {
                    var newItem = Activator.CreateInstance(realDestinationType);
                    Copy(newItem, entry, deepCopy);
                    list.Add(newItem);
                }
                
                if (!destinationType.IsGenericType)
                {
                    var normalArray = Array.CreateInstance(realDestinationType, list.Count);
                    list.CopyTo(normalArray, 0);
                    return normalArray;
                }
                
                if (destinationType.GetGenericTypeDefinition() == typeof(LinkedList<>))
                {
                    var linkedListType = typeof(LinkedList<>).MakeGenericType(realDestinationType);
                    var linkedList = Activator.CreateInstance(linkedListType);
                    var addMethod = linkedList.GetType().GetMethods().First(i => i.Name == "AddLast");
                    foreach (var entry in list)
                    {
                        addMethod.Invoke(linkedList, new[] { entry });
                    }
                    
                    return linkedList;
                }
                
                return list;
            }

            var destination = Activator.CreateInstance(destinationType);
            Copy(destination, source, deepCopy);
            return destination;
        }

        /// <summary>
        /// Copy the data from source to destination.
        /// </summary>
        /// <param name="destination">The destination object.</param>
        /// <param name="source">The source object.</param>
        /// <param name="deepCopy">If true sub-classes are created.</param>
        public static void Copy(object destination, object source, bool deepCopy)
        {
            if (destination == null)
            {
                throw new ArgumentNullException("destination");
            }

            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            var destinationType = destination.GetType();
            var map = GetMap(destinationType, source.GetType());
            if (AllPropertiesMustBeMapped && !map.IsFullyMapped)
            {
                throw new MapException("Not all elements are mapped");
            }

            map.Perform(destination, source, deepCopy);
        }

        /// <summary>
        /// Proxy objects replace the type which makes it confusing to create mapping. So lets just strip the proxy object.
        /// </summary>
        /// <param name="type">The original object.</param>
        /// <returns>The original object without the proxy type.</returns>
        private static Type StripType(Type type)
        {
            return (type.FullName ?? string.Empty).StartsWith("System.Data.Entity.DynamicProxies") ? type.BaseType : type;
        }

        /// <summary>
        /// Get mapping between destination and source type.
        /// </summary>
        /// <param name="destination">The destination type.</param>
        /// <param name="source">The source type.</param>
        /// <returns>Mapping between destination and source.</returns>
        private static MapEntry GetMap(Type destination, Type source)
        {
            destination = StripType(destination);
            source = StripType(source);
            var entry = new KeyValuePair<Type, Type>(destination, source);
            if (Maps.ContainsKey(entry))
            {
                return Maps[entry];
            }

            if (EnforcePreMapping && state != State.Initialising)
            {
                throw new MapException(string.Format(CultureInfo.CurrentCulture, "No mapping exists between {0} and {1}", source, destination));
            }

            var newMap = new MapEntry(destination, source);
            Maps.Add(entry, newMap);
            return newMap;
        }
    }
}
